/*
 * Copyright (C) Francisco Morero Peyrona. All rights reserved.
 *
 * This software is published under the terms of Open Source
 * License version 1.1, a copy of which has been included with this
 * distribution in the License.txt file.
 */

package jdbcmanager.diagram.swing;

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import javax.swing.*;
import javax.swing.event.*;

/**
 * A line with 2 connection sub-lines and 2 gliphs (to represent a relation 1-to-many).<br>
 * Also has a 'selected' status (painted thicker when is selected).
 * <p>
 * Always the this.P1 (ptOrigin) is the '1' part of the relation
 * and this.P2 (ptTarget) is the 'many' part.
 *
 * @author Francisco Morero Peyrona
 */

public class JConnector extends JComponent implements DiagramComponent
{
   private final static String sINFINITE = Character.toString( '\u221E' );
   
   // Line Base Width (small ones close to de table)
   private final static int    nBASE_LINE_WIDTH = 15; // 2 (from table) + 8 (font) + 2 (to line) + 3 (line)
   // Minimum area width  for vertical connectors (setBounds()) 
   private final static int    nMIN_WIDTH = nBASE_LINE_WIDTH * 2;
   // Minimum area height for horizontal connectors (setBounds()) 
   private final static int    nMIN_HEIGHT = 12;    // 12 == 3 (line) + 9 (font)
   
   //------------------------------------------------------
   private final static int nEASY_RIGHT_UP   =  1;
   private final static int nEASY_RIGHT_DOWN =  2;
   private final static int nEASY_LEFT_UP    =  3;
   private final static int nEASY_LEFT_DOWN  =  4;
   private final static int nRARE_RIGHT_UP   =  5;
   private final static int nRARE_RIGHT_DOWN =  6;
   private final static int nRARE_LEFT_UP    =  7;
   private final static int nRARE_LEFT_DOWN  =  8;
   private              int nVectorShape     = -1; // Gets one of values above 
   //------------------------------------------------------
   protected boolean b1toN = true;
   //------------------------------------------------------
   
   private Point   ptOrigin  = null;
   private Point   ptTarget  = null;

   transient private Line2D  linUnion  = new Line2D.Float();  // Connects linBase1 and linBase2 
   transient private Line2D  linBase1  = new Line2D.Float();  // Small line to write the 1 or the N above
   transient private Line2D  linBase2  = new Line2D.Float();  // Small line to write the 1 or the N above
   transient private Point   ptGliph1  = new Point();         // The 1 position in the "1 to N"
   transient private Point   ptGliph2  = new Point();         // The N position in the "1 to N"
   transient private Font    fntGliph  = new Font( "Dialog", Font.PLAIN, 10 );
   transient private boolean bSelected = false;
   
   //-------------------------------------------------------------------------//

   public JConnector()
   {
      this( null, null );
   }

   public JConnector( Point p1, Point p2 )
   {
      setOpaque( false ); // Bounds is a rectangle, but the area is filled only with a line

      this.ptOrigin = (p1 == null ? new Point( 0,0 ) : p1);
      this.ptTarget = (p2 == null ? new Point( 0,0 ) : p2);

      ConnectorMouseListener vml = new ConnectorMouseListener();  
      addMouseListener( vml );
      addMouseMotionListener( vml );

      calculate();
   }

   public Line2D getLine()
   {
      return (new Line2D.Float( this.ptOrigin, this.ptTarget ));
   }

   public void positionP1( int x, int y )
   {
      this.ptOrigin.x = x;
      this.ptOrigin.y = y;

      calculate();
      repaint();
   }

   public void positionP2( int x, int y )
   {
      this.ptTarget.x = x;
      this.ptTarget.y = y;

      calculate();
      repaint();
   }

   public void repositionP1( int nIncrX, int nIncrY )
   {
      this.ptOrigin.x += nIncrX;
      this.ptOrigin.y += nIncrY;

      calculate();
      repaint();
   }

   public void repositionP2( int nIncrX, int nIncrY )
   {
      this.ptTarget.x += nIncrX;
      this.ptTarget.y += nIncrY;

      calculate();
      repaint();
   }

   public boolean is1toN()
   {
      return this.b1toN;
   }

   //-------------------------------------------------------------------------//
   // REQUIRED BY DiagramComponent Interface

   public boolean isSelected()
   {
      return this.bSelected;
   }

   public void setSelected( boolean b )
   {
      this.bSelected = b;
      repaint();
   }

   //-------------------------------------------------------------------------//

   /**
    * To be redefined by subclasses
    */
   protected void showProperties()
   {
   }
   
   /**
    * When this method is called coordinates are already converted to the relative ones.
    * <p>
    * It is needed to redefine this method (instead of "contains( Point p )")
    * because Container.getComponentAt( p ) calls this one.
    */
   public boolean contains( int x, int y )
   {
      if( super.contains( x, y ) )  // Needed because ptLineDist extends the line to infinite
      {
         return this.linBase1.ptLineDist( x, y ) <= 4 ||
                this.linBase2.ptLineDist( x, y ) <= 4 ||
                this.linUnion.ptLineDist( x, y ) <= 4;
      }
      
      return false;
   }

   // This method does not need to call: super.paintComponent( g )
   public void paintComponent( Graphics g )
   {
      Graphics2D graph2D = (Graphics2D) g;

      graph2D.setColor( Color.black );
      graph2D.setStroke( new BasicStroke( this.bSelected ? 3 : 1 ) );

      graph2D.draw( this.linUnion );
      graph2D.draw( this.linBase1  );
      graph2D.draw( this.linBase2  );

      graph2D.setFont( this.fntGliph );
      graph2D.drawString( "1", this.ptGliph1.x, this.ptGliph1.y );
      graph2D.drawString( (b1toN ? sINFINITE : "1"), this.ptGliph2.x, this.ptGliph2.y );
   }

   //-------------------------------------------------------------------------//

   private void calculate()
   {
      setBounds( ptOrigin, ptTarget );   // Must be first

      int nWidth  = getWidth();
      int nHeight = getHeight()-1;  
      
      switch( this.nVectorShape )
      {
         case nEASY_RIGHT_UP :
            linBase1.setLine( 0, nHeight, getWidth()/2, nHeight );
            linBase2.setLine( getWidth()/2, 0, nWidth, 0 );
            ptGliph1.setLocation( 2, nHeight - 3 );
            ptGliph2.setLocation( nWidth - 9, 12 );
            linUnion.setLine( linBase1.getP2(), linBase2.getP1() );
            break;

         case nEASY_RIGHT_DOWN :
            linBase1.setLine( 0, 0, getWidth()/2, 0 );
            linBase2.setLine( getWidth()/2, nHeight, nWidth, nHeight );
            ptGliph1.setLocation( 2, 12 );
            ptGliph2.setLocation( nWidth - 9, nHeight - 3 );
            linUnion.setLine( linBase1.getP2(), linBase2.getP1() );
            break;

         case nEASY_LEFT_UP :
            linBase1.setLine( 0, 0, getWidth()/2, 0 );
            linBase2.setLine( getWidth()/2, nHeight, nWidth, nHeight );
            ptGliph1.setLocation( nWidth - 9, nHeight - 3 );
            ptGliph2.setLocation( 2, 12 );
            linUnion.setLine( linBase1.getP2(), linBase2.getP1() );
            break;

         case nEASY_LEFT_DOWN :
            linBase1.setLine( 0, nHeight, getWidth()/2, nHeight );
            linBase2.setLine( getWidth()/2, 0, nWidth, 0 );
            ptGliph1.setLocation( nWidth - 9, 12 );
            ptGliph2.setLocation( 2, nHeight - 3 );
            linUnion.setLine( linBase1.getP2(), linBase2.getP1() );
            break;

         case nRARE_RIGHT_UP :
            linBase1.setLine( 0, nHeight, nWidth-3, nHeight );
            linBase2.setLine( nWidth - nBASE_LINE_WIDTH, 0, nWidth-3, 0 );
            ptGliph1.setLocation( 2, nHeight - 3 );
            ptGliph2.setLocation( linBase2.getP2().getX() - 8, 12 );
            linUnion.setLine( linBase1.getP2(), linBase2.getP2() );
            break;

         case nRARE_RIGHT_DOWN :
            linBase1.setLine( 0, 0, nWidth-3, 0 );
            linBase2.setLine( nWidth - nBASE_LINE_WIDTH, nHeight, nWidth-3, nHeight );
            ptGliph1.setLocation( 2, 12 );
            ptGliph2.setLocation( linBase2.getP2().getX() - 8, nHeight -3 );
            linUnion.setLine( linBase1.getP2(), linBase2.getP2() );
            break;

         case nRARE_LEFT_UP :
            linBase1.setLine( 0, nHeight, nWidth, nHeight );
            linBase2.setLine( 0, 0, nBASE_LINE_WIDTH, 0 );
            ptGliph1.setLocation( nWidth - 9, nHeight - 3 );
            ptGliph2.setLocation( linBase2.getP1().getX()+2, 12 );
            linUnion.setLine( linBase1.getP1(), linBase2.getP1() );
            break;

         case nRARE_LEFT_DOWN :
            linBase1.setLine( 0, 0, nWidth, 0 );
            linBase2.setLine( 0, nHeight, nBASE_LINE_WIDTH, nHeight );
            ptGliph1.setLocation( nWidth - 9, 12 );
            ptGliph2.setLocation( linBase2.getP2().getX() - 9, nHeight - 3 );
            linUnion.setLine( linBase1.getP1(), linBase2.getP1() );
            break;
      }
   }

   // I had to write this method because "Line2D.Float.getBounds()"
   // aparently does not work properly, or at least as I expected.
   private void setBounds( Point p1, Point p2 )
   {
      /* NEXT If I want that the connectors are updated when the JBoard changes its
              width (2 cols to all cols) these constants should be variables.
      */ 
      final int TABLE_WIDTH_ORIGIN = 124;
      final int TABLE_WIDTH_TARGET = 124;

      Rectangle r = new Rectangle();
    
      r.y      = Math.min( p1.y, p2.y );
      r.height = Math.max( Math.max( p1.y, p2.y ) - r.y, nMIN_HEIGHT );
      
      if( p1.x > p2.x )
      {
         if( p1.x - (p2.x + TABLE_WIDTH_TARGET) < nMIN_WIDTH )
         {
            r.x     = p2.x - nBASE_LINE_WIDTH;      
            r.width = p1.x - r.x;
            
            this.nVectorShape = (p1.y < p2.y) ? nRARE_LEFT_DOWN : nRARE_LEFT_UP;
         }
         else
         {
            r.x     = p2.x + TABLE_WIDTH_TARGET;
            r.width = p1.x - r.x;
            
            this.nVectorShape = (p1.y < p2.y) ? nEASY_LEFT_DOWN : nEASY_LEFT_UP;
         }
      }
      else
      {
         r.x = p1.x + TABLE_WIDTH_ORIGIN;
         
         if( p2.x - p1.x > TABLE_WIDTH_ORIGIN + nMIN_WIDTH )   
         {
            r.width = p2.x - r.x;
            this.nVectorShape = (p1.y < p2.y) ? nEASY_RIGHT_DOWN : nEASY_RIGHT_UP;
         }
         else
         {
            r.width = p2.x + TABLE_WIDTH_TARGET + nBASE_LINE_WIDTH - r.x;
            this.nVectorShape = (p1.y < p2.y) ? nRARE_RIGHT_DOWN : nRARE_RIGHT_UP;
         }
      }
  
      setBounds( r );
   }
 
   //-------------------------------------------------------------------------//
   // INNER CLASS: Mouse Listener
   //-------------------------------------------------------------------------//
   private final class ConnectorMouseListener extends MouseInputAdapter
   {
      public void mousePressed( MouseEvent evt )
      {
         ((JDiagramPane) getParent()).select( JConnector.this );    // All three mouse buttons select the JConnector
      }
      
      public void mouseMoved( MouseEvent evt )
      {
         int nCursor = (contains( evt.getPoint() )) ? Cursor.HAND_CURSOR : Cursor.DEFAULT_CURSOR;
         
         if( getCursor().getType() != nCursor )
            setCursor( Cursor.getPredefinedCursor( nCursor ) );
      }
       
      public void mouseReleased( MouseEvent evt )
      {
         if( evt.getClickCount() == 2 )
            showProperties(); 
      }
   }
}